/*
 * Copyright 2008,2009,2010 Daniel Freitas
 *
 * This file is part of DMF Generic DAO.
 *
 * DMF Generic DAO is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
 * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 *
 * DMF Generic DAO is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with DMF Generic DAO. If not, see
 * <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dmf.genericdao.hibernate;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.hibernate.Query;
import org.hibernate.SessionFactory;

import com.googlecode.dmf.genericdao.AnnotatedQueryNameResolver;
import com.googlecode.dmf.genericdao.DaoQueryExecutor;
import com.googlecode.dmf.genericdao.MethodNameQueryNameResolver;
import com.googlecode.dmf.genericdao.QueryNameResolver;
import com.googlecode.dmf.genericdao.annotaions.NamedParam;

/**
 * {@link DaoQueryExecutor} implementation for hibernate. Execute hibernate mapped queries for methods invoked in the
 * dao interfaces.
 * <p/>
 * Queries must be mapped in hibernate and follow the naming convention
 * <code><i>[interface_fully_qualified_name].[method_name]</pre></i></code>. For example, if a GenericDao sub-interface
 * called <code>com.googlecode.armycreator.datastore.GameDao</code> has a method called
 * <code>listAllRostersByPointRange()</code>, then the correspondinf query must be named
 * <code>com.googlecode.armycreator.datastore.GameDao.listAllRostersByPointRange</code>.
 *
 * @author <a href="mailto:daniel.mfreitas@gmail.com">Daniel Freitas</a>
 */
public class HibernateQueryExecutor implements DaoQueryExecutor {

    private final SessionFactory sessionFactory;
    private final ArrayList<QueryNameResolver> queryNameResolvers;
    private final ArrayList<QueryNameResolver> customQueryNameResolvers;

    /**
     * A {@link DaoQueryExecutor} implementation tailored to find execute hibernate named queries.
     *
     * @param sessionFactory
     *            The hibernate {@link SessionFactory} to be used.
     */
    public HibernateQueryExecutor(final SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
        queryNameResolvers = new ArrayList<QueryNameResolver>();
        queryNameResolvers.add(new AnnotatedQueryNameResolver());
        queryNameResolvers.add(new MethodNameQueryNameResolver());

        customQueryNameResolvers = new ArrayList<QueryNameResolver>();
    }

    /**
     *
     * {@inheritDoc}
     *
     * @see com.googlecode.dmf.genericdao.DaoQueryExecutor#executeQueryForMethod(java.lang.reflect.Method,
     *      java.lang.Object[])
     */
    @Override
    public Object executeQueryForMethod(final Method invokedMethod, final Object... arguments) {

        String queryName = resolveQueryName(customQueryNameResolvers, invokedMethod);

        Query namedQuery = sessionFactory.getCurrentSession().getNamedQuery(queryName);

        Annotation[][] parameterAnnotations = invokedMethod.getParameterAnnotations();

        for (int i = 0; i < parameterAnnotations.length; i++) {
            Annotation[] annotationArray = parameterAnnotations[i];

            for (Annotation parameterAnnotation : annotationArray) {
                if (parameterAnnotation.annotationType().equals(NamedParam.class)) {
                    NamedParam namedParam = (NamedParam) parameterAnnotation;
                    namedQuery.setParameter(namedParam.value(), arguments[i]);
                }
            }
        }

        if (List.class.isAssignableFrom(invokedMethod.getReturnType())) {
            return namedQuery.list();
        } else {
            return namedQuery.uniqueResult();
        }
    }

    private String resolveQueryName(final ArrayList<QueryNameResolver> customQueryNameResolvers,
            final Method invokedMethod) {
        for (QueryNameResolver resolver : customQueryNameResolvers) {
            String queryName = resolver.resolveName(invokedMethod);

            if (queryName != null) {
                return queryName;
            }
        }

        return resolveQueryName(queryNameResolvers, invokedMethod);
    }

    /**
     * Adds a new custom {@link QueryNameResolver} to the list of custom query name resolvers.
     *
     * @param e
     *            The {@link QueryNameResolver} instance to add to the list.
     * @return <tt>true</tt> (as specified by {@link Collection#add(Object)}).
     * @see java.util.ArrayList#add(java.lang.Object)
     */
    public boolean addCustomQueryNameResolver(final QueryNameResolver e) {
        return customQueryNameResolvers.add(e);
    }

    /**
     * Remove all custom {@link QueryNameResolver}s.
     *
     * @see java.util.ArrayList#clear()
     */
    public void clearCustomNameQueryResolvers() {
        customQueryNameResolvers.clear();
    }

    /**
     * Removes the {@link QueryNameResolver} instance from the list of custom query name resolvers.
     *
     *@return <tt>true</tt> if this list contained the specified element.
     *
     * @see java.util.ArrayList#remove(java.lang.Object)
     */
    public boolean removeCustomNameQueryResolver(final Object o) {
        return customQueryNameResolvers.remove(o);
    }

    /**
     * Sets the list of custom query name resolvers. This method exists for compatibility with Spring setter injection.
     * A copy of the list is made so changes to the original list do not affect the internal list.
     * <p/>
     * The list is cleared before inserting the new resolvers.
     *
     * @param resolvers
     *            The list of custom query name resolvers to add.
     */
    public void setCustomQueryNameResolvers(final List<QueryNameResolver> resolvers) {
        customQueryNameResolvers.clear();
        customQueryNameResolvers.addAll(resolvers);
    }

    /**
     * Gets the list of custom query name resolvers. A copy the list is performed so changes in the returned copy does
     * not affect the internal list.
     *
     * @return A copy of the list of custom query name resolvers registered in this query executor.
     */
    public List<QueryNameResolver> getCustomQueryNameResolvers() {
        return Collections.unmodifiableList(customQueryNameResolvers);
    }
}
